import { Buffer } from 'buffer';
import _ from 'lodash';
import is from 'is-type-of';
import * as helpers from './helpers.js';
import { Logger } from '@nestjs/common';

const logger = new Logger('LodashProtocol');
const protocol: any = {};

Object.defineProperties(protocol, {
	FDFS_PROTO_CMD_QUIT: helpers.buildConstProp(82),
	TRACKER_PROTO_CMD_SERVER_LIST_GROUP: helpers.buildConstProp(91),
	TRACKER_PROTO_CMD_SERVER_LIST_STORAGE: helpers.buildConstProp(92),
	TRACKER_PROTO_CMD_SERVER_DELETE_STORAGE: helpers.buildConstProp(93),

	TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE: helpers.buildConstProp(101),
	TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ONE: helpers.buildConstProp(102),
	TRACKER_PROTO_CMD_SERVICE_QUERY_UPDATE: helpers.buildConstProp(103),
	TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITH_GROUP_ONE: helpers.buildConstProp(104),
	TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ALL: helpers.buildConstProp(105),
	TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ALL: helpers.buildConstProp(106),
	TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITH_GROUP_ALL: helpers.buildConstProp(107),
	TRACKER_PROTO_CMD_RESP: helpers.buildConstProp(100),
	FDFS_PROTO_CMD_ACTIVE_TEST: helpers.buildConstProp(111),
	STORAGE_PROTO_CMD_UPLOAD_FILE: helpers.buildConstProp(11),
	STORAGE_PROTO_CMD_DELETE_FILE: helpers.buildConstProp(12),
	STORAGE_PROTO_CMD_SET_METADATA: helpers.buildConstProp(13),
	STORAGE_PROTO_CMD_DOWNLOAD_FILE: helpers.buildConstProp(14),
	STORAGE_PROTO_CMD_GET_METADATA: helpers.buildConstProp(15),
	STORAGE_PROTO_CMD_UPLOAD_SLAVE_FILE: helpers.buildConstProp(21),
	STORAGE_PROTO_CMD_QUERY_FILE_INFO: helpers.buildConstProp(22),
	STORAGE_PROTO_CMD_UPLOAD_APPENDER_FILE: helpers.buildConstProp(23),  //create appender file
	STORAGE_PROTO_CMD_APPEND_FILE: helpers.buildConstProp(24),  //append file
	STORAGE_PROTO_CMD_MODIFY_FILE: helpers.buildConstProp(34),  //modify appender file
	STORAGE_PROTO_CMD_TRUNCATE_FILE: helpers.buildConstProp(36),  //truncate appender file

	FDFS_STORAGE_STATUS_INIT: helpers.buildConstProp(0),
	FDFS_STORAGE_STATUS_WAIT_SYNC: helpers.buildConstProp(1),
	FDFS_STORAGE_STATUS_SYNCING: helpers.buildConstProp(2),
	FDFS_STORAGE_STATUS_IP_CHANGED: helpers.buildConstProp(3),
	FDFS_STORAGE_STATUS_DELETED: helpers.buildConstProp(4),
	FDFS_STORAGE_STATUS_OFFLINE: helpers.buildConstProp(5),
	FDFS_STORAGE_STATUS_ONLINE: helpers.buildConstProp(6),
	FDFS_STORAGE_STATUS_ACTIVE: helpers.buildConstProp(7),
	FDFS_STORAGE_STATUS_NONE: helpers.buildConstProp(99),

	/**
	 * for overwrite all old metadata
	 */
	STORAGE_SET_METADATA_FLAG_OVERWRITE: helpers.buildConstProp('O'),

	/**
	 * for replace, insert when the meta item not exist, otherwise update it
	 */
	STORAGE_SET_METADATA_FLAG_MERGE: helpers.buildConstProp('M'),

	FDFS_PROTO_PKG_LEN_SIZE: helpers.buildConstProp(8),
	FDFS_PROTO_CMD_SIZE: helpers.buildConstProp(1),
	FDFS_GROUP_NAME_MAX_LEN: helpers.buildConstProp(16),
	FDFS_IPADDR_SIZE: helpers.buildConstProp(16),
	FDFS_DOMAIN_NAME_MAX_SIZE: helpers.buildConstProp(128),
	FDFS_VERSION_SIZE: helpers.buildConstProp(6),
	FDFS_STORAGE_ID_MAX_SIZE: helpers.buildConstProp(16),

	FDFS_RECORD_SEPERATOR: helpers.buildConstProp('\u0001'),
	FDFS_FIELD_SEPERATOR: helpers.buildConstProp('\u0002'),

	FDFS_FILE_EXT_NAME_MAX_LEN: helpers.buildConstProp(6),
	FDFS_FILE_PREFIX_MAX_LEN: helpers.buildConstProp(16),
	FDFS_FILE_PATH_LEN: helpers.buildConstProp(10),
	FDFS_FILENAME_BASE64_LENGTH: helpers.buildConstProp(27),
	FDFS_TRUNK_FILE_INFO_LEN: helpers.buildConstProp(16),

	ERR_NO_ENOENT: helpers.buildConstProp(2),
	ERR_NO_EIO: helpers.buildConstProp(5),
	ERR_NO_EBUSY: helpers.buildConstProp(16),
	ERR_NO_EINVAL: helpers.buildConstProp(22),
	ERR_NO_ENOSPC: helpers.buildConstProp(28),
	ECONNREFUSED: helpers.buildConstProp(61),
	ERR_NO_EALREADY: helpers.buildConstProp(114),

	// 成功的STATUS
	HEADER_STATUS_SUCCESS: helpers.buildConstProp(0)
});

Object.defineProperties(protocol, {
	STORAGE_PROTO_CMD_RESP: helpers.buildConstProp(protocol.TRACKER_PROTO_CMD_RESP),

	TRACKER_QUERY_STORAGE_FETCH_BODY_LEN: helpers.buildConstProp(protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE - 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE),
	TRACKER_QUERY_STORAGE_STORE_BODY_LEN: helpers.buildConstProp(protocol.FDFS_GROUP_NAME_MAX_LEN + protocol.FDFS_IPADDR_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE),

	PROTO_HEADER_CMD_INDEX: helpers.buildConstProp(protocol.FDFS_PROTO_PKG_LEN_SIZE),
	PROTO_HEADER_STATUS_INDEX: helpers.buildConstProp(protocol.FDFS_PROTO_PKG_LEN_SIZE + 1),

	HEADER_BYTE_LENGTH: helpers.buildConstProp(protocol.FDFS_PROTO_PKG_LEN_SIZE + 2)
});

// 协议相关的封装方法
_.extend(protocol, {

	FDFS_METHOD_UPLOAD_FILE: 'upload',
	FDFS_METHOD_UPLOAD_APPENDER_FILE: 'uploadAppender',
	FDFS_METHOD_APPEND_FILE: 'append',
	FDFS_METHOD_MODIFY_FILE: 'modify',

	/**
	 * 封装协议头
	 * @param command
	 * @param bodyLength
	 * @param status
	 * @return {Buffer}
	 */
	packHeader: function(command: number, bodyLength: number, status: number): Buffer {
		if (!bodyLength) {
			bodyLength = 0;
		}

		if (!status) {
			status = 0;
		}

		// ----------- 存放1字节的command和1字节的status
		const buffer = Buffer.alloc(2);
		buffer.writeUInt8(command, 0);
		buffer.writeUInt8(status, 1);

		// 生成8bytes存放body length的buffer
		const blBuffer = helpers.number2Buffer(bodyLength, 8);

		// 拼接
		return Buffer.concat([blBuffer, buffer] as any);
	},

	/**
	 * 解析返回的包
	 * @param socket
	 * @param expectedCommand
	 * @param expectedBodyLength
	 * @param callback
	 * @param headerOnly 是否指parse header，下载文件时，解析完header后需将data传递给callback，而非将body接收完
	 */
	recvPacket: function(socket: any, expectedCommand: number, expectedBodyLength: number, callback: Function, headerOnly: boolean) {
		const oriCallback = callback;
		// 收包完毕则关闭掉连接
		callback = function() {
			cleanup();
			if (is.function(oriCallback)) {
				oriCallback.apply(null, arguments);
			}
		};
		const headerBufferLen = protocol.HEADER_BYTE_LENGTH;
		const headerBuffer: any = Buffer.alloc(headerBufferLen);
		// 已填充的length
		let headerBufferFilled = 0;
		// 解析后的header信息
		// {status: , bodyLength: }
		let header: any;
		let bodyBuffer: any;
		// 下1次要copy到bodyBuffer中的起始位置
		let bodyBufferStart = 0;

		socket.on('data', listener);

		function listener(data: Buffer) {
			// --------------- 收到服务器发来消息
			// -------- 解析header
			if (!header) {
				// header parsed
				if (headerBufferFilled + data.length >= headerBufferLen) {
					const len = headerBufferFilled + data.length;
					// 只copy剩下的header部分
					data.copy(headerBuffer, headerBufferFilled, 0, headerBufferLen - headerBufferFilled);
					try {
						header = _parseHeader(headerBuffer, expectedCommand, expectedBodyLength);
						logger.debug('receive server packet header: %j', header);
						// 无body时直接返回
						if (header.bodyLength === 0) {
							callback(null, header);
							return;
						}

						if (headerOnly) {
							oriCallback(null, header);
							if (len > headerBufferLen) {
								oriCallback(null, data.slice(headerBufferLen - headerBufferFilled));
							}
							return;
						}

						bodyBuffer = Buffer.alloc(header.bodyLength);

						// 还有body的数据则填充到body buffer中
						if (len > headerBufferLen) {
							data.copy(bodyBuffer, 0, headerBufferLen - headerBufferFilled);
							bodyBufferStart = len - headerBufferLen;
							// 读取完毕
							if (bodyBufferStart >= header.bodyLength) {
								callback(null, bodyBuffer);
							}
						}

					} catch (err) {
						callback(err);
					}

				} else {
					data.copy(headerBuffer, headerBufferFilled);
					headerBufferFilled += data.length;
				}

			} else {
				// 交由外部处理
				if (headerOnly) {
					oriCallback(null, data);
					return;
				}
				// ---------- 解析body
				data.copy(bodyBuffer, bodyBufferStart);
				bodyBufferStart += data.length;
				// 读取完毕
				if (bodyBufferStart >= header.bodyLength) {
					callback(null, bodyBuffer);
				}
			}
		};

		function cleanup() {
			socket.removeListener('data', listener);
			// 关闭连接
			protocol.closeSocket(socket);
		};
	},

	/**
	 * 给服务器发送关闭指令，同时end socket
	 * @param socket
	 */
	closeSocket: function(socket: any) {
		socket.end(protocol.packHeader(protocol.FDFS_PROTO_CMD_QUIT, 0, 0));
	},

	/**
	 * 封装只有fileId的包，下载和删除文件时使用
	 * @param command
	 * @param group
	 * @param filename
	 * @param charset
	 */
	packFileId: function(command: number, group: string, filename: string, charset: any): Buffer {
		// --------- 封装header
		const fnLength = Buffer.byteLength(filename, charset);
		const bodyLength = protocol.FDFS_GROUP_NAME_MAX_LEN + fnLength;
		const header = protocol.packHeader(command, bodyLength, 0);

		// --------- 封装body
		const body = Buffer.alloc(bodyLength);
		// 默认都填充上0
		body.fill(0);
		const groupBL = Buffer.byteLength(group, charset);
		body.write(group, 0, groupBL, charset);
		body.write(filename, protocol.FDFS_GROUP_NAME_MAX_LEN, fnLength, charset);

		return Buffer.concat([header, body]);
	},

	/**
	 * 封装meta data
	 * 注：返回的是string，由外部写入到buffer中
	 * @param {Object} metaData
	 * @return {String}
	 */
	packMetaData: function(metaData: { [key: string]: string }): string {
		let first = true;
		let result = '';

		Object.keys(metaData).forEach(function(key) {
			if (!first) {
				result += protocol.FDFS_RECORD_SEPERATOR;

			} else {
				first = false;
			}
			const value = metaData[key];
			result += key;
			result += protocol.FDFS_FIELD_SEPERATOR;
			result += value;
		});

		return result;
	},

	/**
	 * raw meta data to structure
	 * @param raw
	 */
	parseMetaData: function(raw: string): { [key: string]: string } {
		const result: { [key: string]: string } = {};
		const md = raw.split(protocol.FDFS_RECORD_SEPERATOR);
		md.forEach(function(item) {
			const arr = item.split(protocol.FDFS_FIELD_SEPERATOR);
			const key = helpers.trim(arr[0]);
			const value = helpers.trim(arr[1]);
			result[key] = value;
		});

		return result;
	}
});

function _parseHeader(headerBuffer: Buffer, expectedCommand: number, expectedBodyLength: number | null): { status: number, bodyLength: number } {
	// validate buffer length
	if (headerBuffer.length !== protocol.FDFS_PROTO_PKG_LEN_SIZE + 2) {
		throw new Error('receive packet size ' + headerBuffer.length + ' is not equal to the expected header size: ' + protocol.FDFS_PROTO_PKG_LEN_SIZE + 2);
	}

	// validate command
	const command = Number('0x' + headerBuffer.toString('hex', protocol.PROTO_HEADER_CMD_INDEX, protocol.PROTO_HEADER_CMD_INDEX + 1));
	if (expectedCommand !== command) {
		throw new Error('receive command: ' + command + ' is not equal to the expected command: ' + expectedCommand);
	}

	// 应用层错误
	const status = Number('0x' + headerBuffer.toString('hex', protocol.PROTO_HEADER_STATUS_INDEX, protocol.PROTO_HEADER_STATUS_INDEX + 1));
	if (status !== protocol.HEADER_STATUS_SUCCESS) {
		const err = new Error('receive packet errno is: ' + status);
		(err as any).code = (err as any).errno = status;
		throw err;
	}

	// validate body length
	const bodyLength = helpers.buffer2Number(headerBuffer, 0);
	if (expectedBodyLength != null && expectedBodyLength !== bodyLength) {
		throw new Error('receive packet body length: ' + bodyLength + ' is not equal to the expected: ' + expectedBodyLength);
	}

	return {
		status: protocol.HEADER_STATUS_SUCCESS,
		bodyLength: bodyLength
	};
}

export default protocol;